<?php
/**
 * Webappkit API
 *
 * This file is part of Webappkit, a packages system for PHP web apps
 * @link http://webappkit.net
 * @package webappkit
 * @author J.Ducastel <nospam0@ducastel.name>
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 */

if (class_exists('Webappkit')) {
    // echo "<p>webappkit defined yet !</p>";
    //return null;
}

/**
* @static
* @link http://code.google.com/p/webappkit/wiki/Kits
* @todo use exceptions
*/
class Webappkit {
    /**
     * @var array Kit instances cache id => instance
     */
    protected static $kits=array();

    /**
     * @var array loaded kits list id => version
     */
    protected static $loaded=array();

    /**
     * @var array kit configurations cache id => cfg array
     */
    protected static $cfgs=array();

    /**
     * @var int total loaded weight, in octets
     */
    public static $loaded_size=0;

    /**
     * @var array classes index for __autoload class => path
     */
    public static $classes=array();

    /**
    * factory : use this to get a kit instance.
    * @param string $kit_id kit id : use / to access subkits
    * @static
    * @return WakKit
    */
    public static function & getKit($kit_id) {
        // cleaning id
        if (!$id=self::checkId($kit_id))
            throw new WakBadKit("invalid kit id $kit_id");
        if (!isset(self::$kits[$id]) and $id) {
            // loading kit libraries
            self::loadKit($id);
            // loading kit config
            $cfg=self::getCfgFromId($id);
            // defining kit's class (default is WakKit)
            $class=$cfg->getClass();
            // building instance of correct class
            if (!class_exists($class))
                throw new WakBadKit("Kit class ($class) does not exists for kit ($id)");
            $kit=&new $class($id);
            // checking if implements the kit interface
            if (!$kit instanceof iWakKit)
                throw new WakBadKit("Class ($class) does not implements kit interface (iWakKit)");
            // storing instance in cache
            self::$kits[$id]=&$kit;
        }
        return self::$kits[$id];
    }

    /**
     * loads a kit libraries and dependancies
     * @access public
     * @static
     * @param string $kit_id
     * @param string $v_range required version range
     * @return string loaded version string
     */
    public static function loadKit($kit_id, $v_range='*') {
        // cleaning id
        if (!$id=self::checkId($kit_id))
            throw new WakBadKit("invalid kit id $kit_id");
        // not loading a kit twice

        if (isset(self::$loaded[$id]))
            return true;
        // echo "<p>loading '$id'</p>";
        $path=self::getPathFromId($id);
        // reading meta/config
        $cfg=self::getCfgFromId($id);
        if (!$cfg->isValid())
            throw new WakBadCfg("invalid config for kit $id");
        // checking for version
        $version=$cfg->getVersion();
        if (!self::isVersionWithinRange($version,$v_range))
            throw new WakBadVersion("kit $id version is $version, $v_range required");
        // else echo "<p>$version in $v_range ??</p>";
        // checking for supported php version
        if (!self::isVersionWithinRange(phpversion(),$cfg->getPhpRange()))
            throw new WakBadPHP("Kit $id does not support PHP version ".phpversion());
        // loading external libraries (kits dependancies)
        foreach ($cfg->getDependencies() as $required => $range) {
            if ($required==$id) {
                // kit requires iself, preventing infinite loop
                continue;
            }
            // try to load
            try {
                self::loadkit($required,$range);
            } catch (WakException $e) {
                // dependency failing
                throw new WakMissingKit("Kit $required is required by $id, could not be loaded because: ".$e->getMessage());
            }
        }
        // loading internal libraries
        foreach ($cfg->getFiles() as $file)
            self::loadlibfile($path.$file);
        // indexing library classes for autoload
        foreach ($cfg->getClasses() as $class => $file) {
            $class=strtolower($class);
            self::$classes[$class]=$path.$file;
            //webappkit::loadlibfile($path.$file);
        }
        // marking as loaded
        // before loading subkits to prevent recursion errors when kit and subkits require each other
        self::$loaded[$id] = $cfg->getVersion();
        // loading subkits
        foreach ($cfg->getSubkits() as $subkit => $range) {
            try {
                self::loadkit($id.'.'.$subkit,$range);
            } catch (WakException $e) {
                throw new WakBadKit("Could not load subkit ($subkit) of ($id). Error was : ".$e->getMessage());
            }
            // echo "<p>loaded subkit $id.$subkit</p>";
        }

        // echo '<pre>'; print_r(self::$loaded); echo '</pre>';
        // echo "<p>loaded $id</p>";
        return self::$loaded[$id];
    }

    /**
    * returns spent time in microseconds, with chosen precision
    * if not defined, defines WAK_START_TIME as reference microtime
    * @param int $precision (default 5)
    * @static
    * @access public
    * @return float spent time since the first call
    */
    public static function timeCounter($precision=5) {
        list($usec, $sec) = explode(" ", microtime());
        $microtime=((float)$usec + (float)$sec);
        if (!defined('WAK_START_TIME'))
            define('WAK_START_TIME',$microtime);
        //echo $microtime;
        $timeCounter=$microtime-WAK_START_TIME;
        return round($timeCounter,$precision);
    }

    /**
     * lists kits in a path.
     * Uses WAK_ROOT_PATH root path as default.
     * @static
     * @param string $path
     * @return array kits ids
     */
    public static function listKits($path=null) {
        if (is_null($path))
            $path=WAK_ROOT_PATH;
        if (!is_dir($path))
            return false;
        $path=realpath($path);
        $kits=array();
        $dir=dir($path);
        while ($file=$dir->read()) {
            if (is_dir($path.'/'.$file)
            and substr($file,-4,4)=='.wak') {
                // looks like a kit
                $id=substr($file,0,-4);
                $kits[]=$id;
            }
        }
        return $kits;
    }

    /**
     * Checks a kit id for usability
     * checks id validity
     * then folder existence, then wak.ini file
     * SHOULD NOT raise any exception
     * @static
     * @param string $id
     * @return bool true if kit exists and is valid, false elsewhere
     */
    public static function checkKit($id) {
        try {
            $id=Webappkit::checkId($id);
            $path=Webappkit::getPathFromId($id);
            if (!file_exists($path.'wak.ini'))
                return false;
            return true;
        }  catch (Exception $e) {
            return false;
        }
    }

    /**
    * checks/cleans kit ID string format
    * accepts both / and . as separators for nesting levels
    * converts / to . in output
    * @static
    * @access public
    * @param string
    * @return string or false if id is invalid
    */
    public static function checkID($id) {
        // converting old / separator to new . standard
        $id=str_replace('/','.',strtolower($id));
        if (!ereg('^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$',$id))
            return false;
        return $id;
    }

    /**
    * get kit's physical path from kit id
    * do not check path existence !!!
    * @static
    * @access public
    * @param string $kit_id
    * @return string path with trailing slash
    */
    public static function getPathFromId($kit_id) {
        if (!$id=self::checkID($kit_id))
            throw new WakBadKit("Webappkit::getPathFromId : bad id {$kit_id}");
            // return false;
        $levels=explode('.',$id);
        $path='';
        foreach ($levels as $l)
            $path.="$l.wak/";
        $path=WAK_ROOT_PATH.$path;
        /*if (!file_exists($path)) {
            // trigger_error("invalid path {$path} for id {$id}");
            // throw new WakBadKit("invalid path {$path} for id {$id}");
            return false;
        }*/
        return $path;
    }

    /**
    * Recursive array merge, replacing values.
    * merges recursively array_1 with array_2, replacing array_1 keys by array_2 keys on collisions.
    * @static
    * @param array &$array_1
    * @param array &$array_2
    * @return &array merged result
    */
    public static function & mergeArrays(&$array_1,&$array_2) {
        $merge=$array_1;
        $keys_1=array_keys($array_1);
        $keys_2=array_keys($array_2);
        foreach ($merge as $key => $value) {
            if (!isset($array_2[$key]))
                continue;
            if (is_array($value) and is_array($array_2[$key])) {
                // appel recursif
                $merge[$key]=self::mergeArrays($value,$array_2[$key]);
            } else {
                $merge[$key]=$array_2[$key];
            }
        }
        foreach ($array_2 as $key => $value)
            if (!isset($merge[$key])) $merge[$key]=$value;
        return $merge;
    }

    /**
    * loads package/kit metadata from ini file
    * @access public
    * @param string $id
    * @return WakKitCfg
    * @todo checking content format
    */
    public static function getCfgFromId($id) {
        if (!isset(self::$cfgs[$id])) {
            self::$cfgs[$id]=new WakKitCfg($id);
            if (!self::$cfgs[$id]->isValid())
                throw new WakBadCfg("invalid config for kit $id");
        }
        return self::$cfgs[$id];
    }

    /**
     * loads a library file.
     * can switch file upon current php version : if $file ends by php*, will load .php4, .php5 etc
     * @param string $file full physical path
     * @return bool
     */
    public static function loadLibFile($file) {
        // auto php version ?
        if (substr($file,-1,1)=='*') {
            $php=substr(phpversion(),0,1);
            $file=substr($file,0,-1).$php;
        }
        // checking existence
        if (!file_exists($file)) {
            // trigger_error("can't include file $file, don't exist");
            throw new WakMissingFile("can't include file $file, don't exist",500);
            return false;
        }
        self::$loaded_size+=filesize($file);
        include_once($file);
        return true;
    }

    /**
     * loads a class definition file
     * it has to be indexed within a kit's [load_classes] section
     * @param string $class class name
     * @return bool
     */
    public static function loadClass($class) {
        $class=strtolower($class);
        if (!isset(self::$classes[$class])) {
            return false;
        }
        return self::loadLibFile(self::$classes[$class]);
    }

    /**
     * checks that a version string is within version range
     * @static
     * @access public
     * @param string $version version string to check x or x.y or x.y.* etc
     * @param string $range version range min-max or minAndMax or min+ or max-
     * @return bool true if in range, false elsewhere
     */
    public static function isVersionWithinRange($version,$range) {
        // parsing range
        switch (true) {
            case ereg('^[0-9\.\*]+$',$range): // minAndMax (with *=jokers)
                $min=$range; $max=$range;
                break;
            case substr($range,-1,1)=='+': // min+
                $min=substr($range,0,-1);
                $max=null;
                break;
            default: // min-max
                $range=explode('-',$range);
                $min=$range[0];
                $max=isset($range[1])?$range[1]:null;
        }
        // is under minimum ?
        if (self::cmpVersionStrings($version,$min)<0)
            return false; // under min
        // is over max ?
        if ($max and self::cmpVersionStrings($max,$version)<0)
            return false;
        // is within range
        return true;
    }

    /**
     * compares version strings
     * @static
     * @access public
     * @param string $version1
     * @param string $version2
     * @return int 1 if $version1>$version2, 0 if $version1==$version2, -1 if $version1<$version2
     */
    public static function cmpVersionStrings($version1,$version2) {
        $v1=explode('.',$version1);
        $v2=explode('.',$version2);
        // looping through segments
        for ($n=0; $n<count($v1); $n++) {
            switch (true) {
                case ($v1[$n]=='*'): // joker, versions match
                    return 0;
                case !isset($v1[$n]): // v2 longer than v1, so over
                    return -1;
                case !isset($v2[$n]): // v1 longer than v2, so over
                    return 1;
                case $v1[$n]>$v2[$n]: // v1 over v2
                    return 1;
                case $v1[$n]<$v2[$n]: // v2 over v1
                    return -1;
                case $v1[$n]==$v2[$n]: // segment match, continue
                    continue;
            }
        }
        // versions are identical
        return 0;
    }
}

/**
 * auto-loading of indexed classes
 * @param string $class class name
 */
if (function_exists('spl_autoload_register')) {
    spl_autoload_register(array('Webappkit','loadClass'));
} else {
    function __autoload($class) {
        return Webappkit::loadClass($class);
    }
}
